import * as Cesium from 'cesium';
import GaussianSplatSorter from './GaussianSplatSorter';
import GaussianSplatTextureGenerator from './GaussianSplatTextureGenerator';
import { VertexAttributeSemanticGaussian } from './fixVertexAttributeSemantic';
import { PixelFormatGaussianSplatting } from './fixPixelFormat';
import { TextureGaussianSplatting } from './fixTexture';

export default function fixModel() {
    Object.defineProperties(Cesium.Model.prototype, {
        _enableShowGaussianSplatting: {
            value: true
        },
        // });
        // Object.defineProperties(Cesium.Model, {
        showGaussianSplatting: {
            value: true
        },
        enableShowGaussianSplatting: {
            get: function () {
                return this._enableShowGaussianSplatting;
            },
            set: function (value) {
                if (value !== this._enableShowGaussianSplatting) {
                    this.resetDrawCommands();
                }
                this._enableShowGaussianSplatting = value;
                // Warning for improper setup of gaussian splatting
                // @ts-ignore
                if (value && this.type !== Cesium.ModelType.GLTF && this.type !== Cesium.ModelType.TILE_GLTF) {
                    //@ts-ignore
                    Cesium.oneTimeWarning(
                        "model-enable-show-gaussian-splatting-ignored",
                        "enableShowGaussian splatting must be used with a glTF model that has the KHR_gaussian_splatting extension",
                    );
                }
            }
        }
    });
    //@ts-ignore
    Cesium.Model.fromGltfAsync = async function (options: any) {
        //@ts-ignore
        options = Cesium.defaultValue(options, Cesium.defaultValue.EMPTY_OBJECT);

        //>>includeStart('debug', pragmas.debug);
        if (!Cesium.defined(options.url) && !Cesium.defined(options.gltf)) {
            throw new Cesium.DeveloperError("options.url is required.");
        }
        //>>includeEnd('debug');

        // options.gltf is used internally for 3D Tiles. It can be a Resource, a URL
        // to a glTF/glb file, a binary glTF buffer, or a JSON object containing the
        // glTF contents.
        const gltf = Cesium.defaultValue(options.url, options.gltf);

        const loaderOptions = {
            releaseGltfJson: options.releaseGltfJson,
            asynchronous: options.asynchronous,
            incrementallyLoadTextures: options.incrementallyLoadTextures,
            upAxis: options.upAxis,
            forwardAxis: options.forwardAxis,
            loadAttributesFor2D: options.projectTo2D,
            enablePick: options.enablePick,
            loadIndicesForWireframe: options.enableDebugWireframe,
            loadPrimitiveOutline: options.enableShowOutline,
            loadForClassification: Cesium.defined(options.classificationType),
            loadGaussianSplatting: options.enableShowGaussianSplatting,
        };

        const basePath = Cesium.defaultValue(options.basePath, "");
        //@ts-ignore
        const baseResource = Cesium.Resource.createIfNeeded(basePath);

        if (Cesium.defined(gltf.asset)) {
            //@ts-ignore
            loaderOptions.gltfJson = gltf;
            //@ts-ignore
            loaderOptions.baseResource = baseResource;
            //@ts-ignore
            loaderOptions.gltfResource = baseResource;
        } else if (gltf instanceof Uint8Array) {
            //@ts-ignore
            loaderOptions.typedArray = gltf;
            //@ts-ignore
            loaderOptions.baseResource = baseResource;
            //@ts-ignore
            loaderOptions.gltfResource = baseResource;
        } else {
            //@ts-ignore
            loaderOptions.gltfResource = Cesium.Resource.createIfNeeded(gltf);
        }
        //@ts-ignore
        const loader = new Cesium.GltfLoader(loaderOptions);

        const is3DTiles = Cesium.defined(options.content);
        //@ts-ignore
        const type = is3DTiles ? Cesium.ModelType.TILE_GLTF : Cesium.ModelType.GLTF;
        //@ts-ignore
        const resource = loaderOptions.gltfResource;

        const modelOptions = makeModelOptions(loader, type, options);
        modelOptions.resource = resource;
        //@ts-ignore
        modelOptions.environmentMapOptions = options.environmentMapOptions;

        try {
            // This load the gltf JSON and ensures the gltf is valid
            // Further resource loading is handled synchronously in loader.process(), and requires
            // hooking into model's update() as the frameState is needed
            await loader.load();
        } catch (error) {
            loader.destroy();
            //@ts-ignore
            throw Cesium.ModelUtility.getError("model", resource, error);
        }

        const gltfCallback = options.gltfCallback;
        if (Cesium.defined(gltfCallback)) {
            //>>includeStart('debug', pragmas.debug);
            Cesium.Check.typeOf.func("options.gltfCallback", gltfCallback);
            //>>includeEnd('debug');

            gltfCallback(loader.gltfJson);
        }
        //@ts-ignore
        const model = new Cesium.Model(modelOptions);
        //@ts-ignore
        const resourceCredits = model._resource.credits;
        if (Cesium.defined(resourceCredits)) {
            const length = resourceCredits.length;
            for (let i = 0; i < length; i++) {
                //@ts-ignore
                model._resourceCredits.push(Cesium.Credit.clone(resourceCredits[i]));
            }
        }

        return model;
    }
    //@ts-ignore
    Cesium.Model.prototype.update = function (frameState: Cesium.FrameState) {
        let finishedProcessing = false;
        try {
            // Keep processing the model every frame until the main resources
            // (buffer views) and textures (which may be loaded asynchronously)
            // are processed.
            finishedProcessing = processLoader(this, frameState);
        } catch (error) {
            //@ts-ignore
            if (!this._loader.incrementallyLoadTextures && error.name === "TextureError") {
                handleError(this, error);
            } else {
                //@ts-ignore
                const runtimeError = Cesium.ModelUtility.getError(
                    "model",
                    //@ts-ignore
                    this._resource,
                    error,
                );
                handleError(this, runtimeError);
            }
        }

        // A custom shader may have to load texture uniforms.
        updateCustomShader(this, frameState);

        // Environment maps, specular maps, and spherical harmonics may need to be updated or regenerated
        updateEnvironmentMap(this, frameState);

        // The image-based lighting may have to load texture uniforms
        // for specular maps.
        updateImageBasedLighting(this, frameState);
        //@ts-ignore
        if (!this._resourcesLoaded && finishedProcessing) {
            //@ts-ignore
            this._resourcesLoaded = true;
            //@ts-ignore
            const components = this._loader.components;
            if (!Cesium.defined(components)) {
                //@ts-ignore
                if (this._loader.isUnloaded()) {
                    return;
                }
                //@ts-ignore

                const error = Cesium.ModelUtility.getError(
                    "model",
                    //@ts-ignore

                    this._resource,
                    new Cesium.RuntimeError("Failed to load model."),
                );
                handleError(this, error);
                //@ts-ignore
                this._rejectLoad = this._rejectLoad && this._rejectLoad(error);
            }

            const structuralMetadata = components.structuralMetadata;
            if (
                Cesium.defined(structuralMetadata) &&
                structuralMetadata.propertyTableCount > 0
            ) {
                createModelFeatureTables(this, structuralMetadata);
            }
            //@ts-ignore
            const sceneGraph = new Cesium.ModelSceneGraph({
                model: this,
                modelComponents: components,
            });
            //@ts-ignore
            this._sceneGraph = sceneGraph;
            //@ts-ignore
            this._gltfCredits = sceneGraph.components.asset.credits;
        }

        // Short-circuit if the model resources aren't ready or the scene
        // is currently morphing.
        //@ts-ignore
        if (!this._resourcesLoaded || frameState.mode === Cesium.SceneMode.MORPHING) {
            return;
        }

        updateFeatureTableId(this);
        updateStyle(this);
        updateFeatureTables(this, frameState);
        updatePointCloudShading(this);
        updateSilhouette(this, frameState);
        updateSkipLevelOfDetail(this, frameState);
        updateClippingPlanes(this, frameState);
        updateClippingPolygons(this, frameState);
        updateSceneMode(this, frameState);
        updateFog(this, frameState);
        updateVerticalExaggeration(this, frameState);
        updateGaussianSplatting(this, frameState);
        //@ts-ignore
        this._defaultTexture = frameState.context.defaultTexture;

        buildDrawCommands(this, frameState);
        updateModelMatrix(this, frameState);

        // Many features (e.g. image-based lighting, clipping planes) depend on the model
        // matrix being updated for the current height reference, so update it first.
        updateClamping(this);

        updateBoundingSphereAndScale(this, frameState);
        updateReferenceMatrices(this, frameState);

        // This check occurs after the bounding sphere has been updated so that
        // zooming to the bounding sphere can account for any modifications
        // from the clamp-to-ground setting.
        //@ts-ignore
        if (!this._ready) {
            // Set the model as ready after the first frame render since the user might set up events subscribed to
            // the post render event, and the model may not be ready for those past the first frame.
            frameState.afterRender.push(() => {
                //@ts-ignore
                this._ready = true;
                //@ts-ignore
                this._readyEvent.raiseEvent(this);
            });

            // Don't render until the next frame after the ready event has been raised.
            return;
        }

        if (
            //@ts-ignore
            this._loader.incrementallyLoadTextures &&
            //@ts-ignore
            !this._texturesLoaded &&
            //@ts-ignore
            this._loader.texturesLoaded
        ) {
            // Re-run the pipeline so texture memory statistics are re-computed
            //@ts-ignore
            this.resetDrawCommands();
            //@ts-ignore
            this._texturesLoaded = true;
            //@ts-ignore
            this._texturesReadyEvent.raiseEvent(this);
        }

        updatePickIds(this);

        // Update the scene graph and draw commands for any changes in model's properties
        // (e.g. model matrix, back-face culling)
        updateSceneGraph(this, frameState);
        updateShowCreditsOnScreen(this);
        submitDrawCommands(this, frameState);
    };
    function generateSplatTexture(primitive: any, frameState: Cesium.FrameState) {
        primitive.gaussianSplatTexturePending = true;
        const promise = GaussianSplatTextureGenerator.generateFromAttrs({
            attributes: {
                positions: new Float32Array(
                    //@ts-ignore
                    Cesium.ModelUtility.getAttributeBySemantic(
                        primitive,
                        //@ts-ignore
                        VertexAttributeSemanticGaussian.POSITION,
                    ).typedArray,
                ),
                scales: new Float32Array(
                    //@ts-ignore
                    Cesium.ModelUtility.getAttributeBySemantic(
                        primitive,
                        //@ts-ignore
                        VertexAttributeSemanticGaussian.SCALE,
                    ).typedArray,
                ),
                rotations: new Float32Array(
                    //@ts-ignore
                    Cesium.ModelUtility.getAttributeBySemantic(
                        primitive,
                        //@ts-ignore
                        VertexAttributeSemanticGaussian.ROTATION,
                    ).typedArray,
                ),
                colors: new Uint8Array(
                    //@ts-ignore
                    Cesium.ModelUtility.getAttributeBySemantic(
                        primitive,
                        //@ts-ignore
                        VertexAttributeSemanticGaussian.COLOR,
                    ).typedArray,
                ),
            },
            count: primitive.attributes[0].count,
        });

        if (promise === undefined) {
            primitive.gaussianSplatTexturePending = false;
            return;
        }

        promise.then((splatTextureData: any) => {
            const splatTex = new TextureGaussianSplatting({
                context: frameState.context,
                //@ts-ignore
                source: {
                    width: splatTextureData.width,
                    height: splatTextureData.height,
                    arrayBufferView: splatTextureData.data,
                },
                preMultiplyAlpha: false,
                skipColorSpaceConversion: true,
                pixelFormat: PixelFormatGaussianSplatting.RGBA_INTEGER,
                pixelDatatype: Cesium.PixelDatatype.UNSIGNED_INT,
                flipY: false,
                //@ts-ignore
                sampler: Cesium.Sampler.NEAREST,
            });
            const count = primitive.attributes[0].count;
            //@ts-ignore
            const attribute = new Cesium.ModelComponents.Attribute();

            //index attribute for indexing into attribute texture
            attribute.name = "_SPLAT_INDEXES";
            attribute.typedArray = new Uint32Array([...Array(count).keys()]);
            attribute.componentDatatype = Cesium.ComponentDatatype.UNSIGNED_INT;
            //@ts-ignore
            attribute.type = Cesium.AttributeType.SCALAR;
            attribute.normalized = false;
            attribute.count = count;
            attribute.constant = 0;
            attribute.instanceDivisor = 1;

            primitive.attributes.push(attribute);
            primitive.gaussianSplatTexture = splatTex;
            primitive.hasGaussianSplatTexture = true;
            primitive.needsGaussianSplatTexture = false;
            primitive.gaussianSplatTexturePending = false;
        });
    }
    const scratchSplatMatrix = new Cesium.Matrix4();
    function updateGaussianSplatting(model: Cesium.Model, frameState: Cesium.FrameState) {
        let prim;
        //@ts-ignore
        for (let i = 0; i < model.sceneGraph.components.nodes.length; i++) {
            //@ts-ignore
            for (let j = 0; j < model.sceneGraph.components.nodes[i].primitives.length; j++) {
                //@ts-ignore
                const primitive = model.sceneGraph.components.nodes[i].primitives[j];
                if (primitive.isGaussianSplatPrimitive) {
                    prim = primitive;
                    break;
                }
            }
        }

        if (!Cesium.defined(prim)) {
            return;
        }

        if (prim.needsGaussianSplatTexture) {
            if (!prim.gaussianSplatTexturePending) {
                generateSplatTexture(prim, frameState);
            }
            return;
        }

        Cesium.Matrix4.multiply(
            frameState.camera.viewMatrix,
            model.modelMatrix,
            scratchSplatMatrix,
        );

        if (!prim?.hasGaussianSplatTexture) {
            //@ts-ignore
            model.resetDrawCommands();
            return;
        }

        const idxAttr = prim.attributes.find((a: any) => a.name === "_SPLAT_INDEXES");
        //@ts-ignore
        const posAttr = Cesium.ModelUtility.getAttributeBySemantic(prim, VertexAttributeSemanticGaussian.POSITION);

        const promise = GaussianSplatSorter.radixSortIndexes({
            primitive: {
                positions: new Float32Array(posAttr.typedArray),
                modelView: Float32Array.from(scratchSplatMatrix),
                count: idxAttr.count,
            },
            sortType: "Index",
        });

        if (promise === undefined) {
            return;
        }

        promise.catch((err) => {
            throw err;
        });
        promise.then((sortedData) => {
            idxAttr.typedArray = sortedData;
            //@ts-ignore
            model.resetDrawCommands();
        });
    }
    function makeModelOptions(loader: any, modelType: any, options: any) {
        return {
            loader: loader,
            type: modelType,
            resource: options.resource,
            show: options.show,
            modelMatrix: options.modelMatrix,
            scale: options.scale,
            enableVerticalExaggeration: options.enableVerticalExaggeration,
            minimumPixelSize: options.minimumPixelSize,
            maximumScale: options.maximumScale,
            id: options.id,
            allowPicking: options.allowPicking,
            clampAnimations: options.clampAnimations,
            shadows: options.shadows,
            debugShowBoundingVolume: options.debugShowBoundingVolume,
            enableDebugWireframe: options.enableDebugWireframe,
            debugWireframe: options.debugWireframe,
            cull: options.cull,
            opaquePass: options.opaquePass,
            customShader: options.customShader,
            content: options.content,
            heightReference: options.heightReference,
            scene: options.scene,
            distanceDisplayCondition: options.distanceDisplayCondition,
            color: options.color,
            colorBlendAmount: options.colorBlendAmount,
            colorBlendMode: options.colorBlendMode,
            silhouetteColor: options.silhouetteColor,
            silhouetteSize: options.silhouetteSize,
            enableShowOutline: options.enableShowOutline,
            showOutline: options.showOutline,
            outlineColor: options.outlineColor,
            clippingPlanes: options.clippingPlanes,
            clippingPolygons: options.clippingPolygons,
            lightColor: options.lightColor,
            imageBasedLighting: options.imageBasedLighting,
            backFaceCulling: options.backFaceCulling,
            credit: options.credit,
            showCreditsOnScreen: options.showCreditsOnScreen,
            splitDirection: options.splitDirection,
            projectTo2D: options.projectTo2D,
            enablePick: options.enablePick,
            featureIdLabel: options.featureIdLabel,
            instanceFeatureIdLabel: options.instanceFeatureIdLabel,
            pointCloudShading: options.pointCloudShading,
            classificationType: options.classificationType,
            pickObject: options.pickObject,
            showGaussianSplatting: options.showGaussianSplatting,
        };
    }
}
//#region 原始代码，无改动
function handleError(model: any, error: any) {
    if (model._errorEvent.numberOfListeners > 0) {
        model._errorEvent.raiseEvent(error);
        return;
    }

    console.log(error);
}
function processLoader(model: any, frameState: Cesium.FrameState) {
    if (!model._resourcesLoaded || (model._loader.incrementallyLoadTextures && !model._texturesLoaded)) {
        // Ensures frames continue to render in requestRender mode while resources are processing
        frameState.afterRender.push(() => true);
        return model._loader.process(frameState);
    }
    return true;
}
function updateCustomShader(model: any, frameState: Cesium.FrameState) {
    if (Cesium.defined(model._customShader)) {
        model._customShader.update(frameState);
    }
}
function updateEnvironmentMap(model: any, frameState: Cesium.FrameState) {
    const environmentMapManager = model._environmentMapManager;
    //@ts-ignore
    const picking = frameState.passes.pick || frameState.passes.pickVoxel;
    if (model._ready && environmentMapManager.owner === model && !picking) {
        environmentMapManager.position = model._boundingSphere.center;
        environmentMapManager.shouldUpdate =
            !Cesium.defined(model._imageBasedLighting.sphericalHarmonicCoefficients) ||
            !Cesium.defined(model._imageBasedLighting.specularEnvironmentMaps);
        environmentMapManager.update(frameState);

        if (environmentMapManager.shouldRegenerateShaders) {
            model.resetDrawCommands();
        }
    }
}

function updateImageBasedLighting(model: any, frameState: Cesium.FrameState) {
    model._imageBasedLighting.update(frameState);
    if (model._imageBasedLighting.shouldRegenerateShaders) {
        model.resetDrawCommands();
    }
}

function updateFeatureTableId(model: any) {
    if (!model._featureTableIdDirty) {
        return;
    }
    model._featureTableIdDirty = false;
    const components = model._sceneGraph.components;
    const structuralMetadata = components.structuralMetadata;

    if (
        Cesium.defined(structuralMetadata) &&
        structuralMetadata.propertyTableCount > 0
    ) {
        model.featureTableId = selectFeatureTableId(components, model);

        // Mark the style dirty to re-apply it and reflect the new feature ID table.
        model._styleDirty = true;

        // Trigger a rebuild of the draw commands.
        model.resetDrawCommands();
    }
}

function updateStyle(model: any) {
    if (model._styleDirty) {
        model.applyStyle(model._style);
        model._styleDirty = false;
    }
}

function updateFeatureTables(model: any, frameState: Cesium.FrameState) {
    const featureTables = model._featureTables;
    const length = featureTables.length;

    let styleCommandsNeededDirty = false;
    for (let i = 0; i < length; i++) {
        featureTables[i].update(frameState);
        // Check if the types of style commands needed have changed and trigger a reset of the draw commands
        // to ensure that translucent and opaque features are handled in the correct passes.
        if (featureTables[i].styleCommandsNeededDirty) {
            styleCommandsNeededDirty = true;
        }
    }

    if (styleCommandsNeededDirty) {
        updateStyleCommandsNeeded(model);
    }
}

function updateStyleCommandsNeeded(model: any) {
    const featureTable = model.featureTables[model.featureTableId];
    //@ts-ignore
    model._styleCommandsNeeded = Cesium.StyleCommandsNeeded.getStyleCommandsNeeded(
        featureTable.featuresLength,
        featureTable.batchTexture.translucentFeaturesLength,
    );
}

function updatePointCloudShading(model: any) {
    const pointCloudShading = model.pointCloudShading;

    // Check if the shader needs to be updated for point cloud attenuation
    // settings.
    if (pointCloudShading.attenuation !== model._attenuation) {
        model.resetDrawCommands();
        model._attenuation = pointCloudShading.attenuation;
    }
    //@ts-ignore
    if (pointCloudShading.backFaceCulling !== model._pointCloudBackFaceCulling) {
        model.resetDrawCommands();
        model._pointCloudBackFaceCulling = pointCloudShading.backFaceCulling;
    }
}
function selectFeatureTableId(components: any, model: any) {
    const featureIdLabel = model._featureIdLabel;
    const instanceFeatureIdLabel = model._instanceFeatureIdLabel;

    let i, j;
    let featureIdAttribute;

    let node;
    // Scan the nodes till we find one with instances, get the feature table ID
    // if the feature ID attribute of the user-selected index is present.
    for (i = 0; i < components.nodes.length; i++) {
        node = components.nodes[i];
        if (Cesium.defined(node.instances)) {
            //@ts-ignore
            featureIdAttribute = Cesium.ModelUtility.getFeatureIdsByLabel(
                node.instances.featureIds,
                instanceFeatureIdLabel,
            );
            if (
                Cesium.defined(featureIdAttribute) &&
                Cesium.defined(featureIdAttribute.propertyTableId)
            ) {
                return featureIdAttribute.propertyTableId;
            }
        }
    }

    // Scan the primitives till we find one with textures or attributes, get the feature table ID
    // if the feature ID attribute/texture of the user-selected index is present.
    for (i = 0; i < components.nodes.length; i++) {
        node = components.nodes[i];
        for (j = 0; j < node.primitives.length; j++) {
            const primitive = node.primitives[j];
            //@ts-ignore
            const featureIds = Cesium.ModelUtility.getFeatureIdsByLabel(
                primitive.featureIds,
                featureIdLabel,
            );

            if (Cesium.defined(featureIds)) {
                return featureIds.propertyTableId;
            }
        }
    }

    // If there's only one feature table, then select it by default. This is
    // to ensure backwards compatibility with the older handling of b3dm models.
    if (model._featureTables.length === 1) {
        return 0;
    }
}
function createModelFeatureTables(model: any, structuralMetadata: any) {
    const featureTables = model._featureTables;
    const propertyTables = structuralMetadata.propertyTables;
    const length = propertyTables.length;
    for (let i = 0; i < length; i++) {
        const propertyTable = propertyTables[i];
        //@ts-ignore
        const modelFeatureTable = new Cesium.ModelFeatureTable({
            model: model,
            propertyTable: propertyTable,
        });

        featureTables.push(modelFeatureTable);
    }

    return featureTables;
}
function updateSilhouette(model: any, frameState: any) {
    if (model._silhouetteDirty) {
        // Only rebuild draw commands if silhouettes are supported in the first place.
        if (supportsSilhouettes(frameState)) {
            model.resetDrawCommands();
        }

        model._silhouetteDirty = false;
    }
}
function supportsSilhouettes(frameState: any) {
    return frameState.context.stencilBuffer;
}
function updateSkipLevelOfDetail(model: any, frameState: any) {
    const skipLevelOfDetail = model.hasSkipLevelOfDetail(frameState);
    if (skipLevelOfDetail !== model._skipLevelOfDetail) {
        model.resetDrawCommands();
        model._skipLevelOfDetail = skipLevelOfDetail;
    }
}

function updateClippingPlanes(model: any, frameState: any) {
    // Update the clipping planes collection / state for this model to detect any changes.
    let currentClippingPlanesState = 0;
    if (model.isClippingEnabled()) {
        if (model._clippingPlanes.owner === model) {
            model._clippingPlanes.update(frameState);
        }
        currentClippingPlanesState = model._clippingPlanes.clippingPlanesState;
    }

    if (currentClippingPlanesState !== model._clippingPlanesState) {
        model.resetDrawCommands();
        model._clippingPlanesState = currentClippingPlanesState;
    }
}

function updateClippingPolygons(model: any, frameState: any) {
    // Update the clipping polygon collection / state for this model to detect any changes.
    let currentClippingPolygonsState = 0;
    if (model.isClippingPolygonsEnabled()) {
        if (model._clippingPolygons.owner === model) {
            model._clippingPolygons.update(frameState);
            model._clippingPolygons.queueCommands(frameState);
        }
        currentClippingPolygonsState =
            model._clippingPolygons.clippingPolygonsState;
    }

    if (currentClippingPolygonsState !== model._clippingPolygonsState) {
        model.resetDrawCommands();
        model._clippingPolygonsState = currentClippingPolygonsState;
    }
}

function updateSceneMode(model: any, frameState: any) {
    if (frameState.mode !== model._sceneMode) {
        if (model._projectTo2D) {
            model.resetDrawCommands();
        } else {
            model._updateModelMatrix = true;
        }
        model._sceneMode = frameState.mode;
    }
}

function updateFog(model: any, frameState: any) {
    const fogRenderable = frameState.fog.enabled && frameState.fog.renderable;
    if (fogRenderable !== model._fogRenderable) {
        model.resetDrawCommands();
        model._fogRenderable = fogRenderable;
    }
}

function updateVerticalExaggeration(model: any, frameState: any) {
    if (model.enableVerticalExaggeration) {
        const verticalExaggerationNeeded = frameState.verticalExaggeration !== 1.0;
        if (model.hasVerticalExaggeration !== verticalExaggerationNeeded) {
            model.resetDrawCommands();
            model._hasVerticalExaggeration = verticalExaggerationNeeded;
        }
    } else if (model.hasVerticalExaggeration) {
        model.resetDrawCommands(); //if verticalExaggeration was on, reset.
        model._hasVerticalExaggeration = false;
    }
}

function buildDrawCommands(model: any, frameState: any) {
    if (!model._drawCommandsBuilt) {
        model.destroyPipelineResources();
        model._sceneGraph.buildDrawCommands(frameState);
        model._drawCommandsBuilt = true;
    }
}

function updateModelMatrix(model: any, frameState: any) {
    // This is done without a dirty flag so that the model matrix can be updated in-place
    // without needing to use a setter.
    if (!Cesium.Matrix4.equals(model.modelMatrix, model._modelMatrix)) {
        //>>includeStart('debug', pragmas.debug);
        if (frameState.mode !== Cesium.SceneMode.SCENE3D && model._projectTo2D) {
            throw new Cesium.DeveloperError(
                "Model.modelMatrix cannot be changed in 2D or Columbus View if projectTo2D is true.",
            );
        }
        //>>includeEnd('debug');
        model._updateModelMatrix = true;
        model._modelMatrix = Cesium.Matrix4.clone(model.modelMatrix, model._modelMatrix);
    }
}

const scratchPosition = new Cesium.Cartesian3();
const scratchCartographic = new Cesium.Cartographic();
const scratchUpdateHeightCartesian = new Cesium.Cartesian3();

function updateClamping(model: any) {
    if (
        !model._updateModelMatrix &&
        !model._heightDirty &&
        model._minimumPixelSize === 0.0
    ) {
        return;
    }

    if (Cesium.defined(model._removeUpdateHeightCallback)) {
        model._removeUpdateHeightCallback();
        model._removeUpdateHeightCallback = undefined;
    }

    const scene = model._scene;
    if (!Cesium.defined(scene) || model.heightReference === Cesium.HeightReference.NONE) {
        //>>includeStart('debug', pragmas.debug);
        if (model.heightReference !== Cesium.HeightReference.NONE) {
            throw new Cesium.DeveloperError(
                "Height reference is not supported without a scene.",
            );
        }
        //>>includeEnd('debug');
        model._clampedModelMatrix = undefined;
        return;
    }

    const ellipsoid = Cesium.defaultValue(scene.ellipsoid, Cesium.Ellipsoid.default);

    // Compute cartographic position so we don't recompute every update
    const modelMatrix = model.modelMatrix;
    scratchPosition.x = modelMatrix[12];
    scratchPosition.y = modelMatrix[13];
    scratchPosition.z = modelMatrix[14];
    const cartoPosition = ellipsoid.cartesianToCartographic(scratchPosition);

    if (!Cesium.defined(model._clampedModelMatrix)) {
        model._clampedModelMatrix = Cesium.Matrix4.clone(modelMatrix, new Cesium.Matrix4());
    }

    // Install callback to handle updating of terrain tiles
    model._removeUpdateHeightCallback = scene.updateHeight(
        cartoPosition,
        getUpdateHeightCallback(model, ellipsoid, cartoPosition),
        model.heightReference,
    );

    // Set the correct height now
    const height = scene.getHeight(cartoPosition, model.heightReference);
    if (Cesium.defined(height)) {
        // Get callback with cartoPosition being the non-clamped position
        const callback = getUpdateHeightCallback(model, ellipsoid, cartoPosition);

        // Compute the clamped cartesian and call updateHeight callback
        Cesium.Cartographic.clone(cartoPosition, scratchCartographic);
        scratchCartographic.height = height;
        callback(scratchCartographic);
    }

    model._heightDirty = false;
    model._updateModelMatrix = true;
}
function getUpdateHeightCallback(model: any, ellipsoid: any, originalPostition: any) {
    return function (clampedPosition: any) {
        if (isHeightReferenceRelative(model.heightReference)) {
            clampedPosition.height += originalPostition.height;
        }

        ellipsoid.cartographicToCartesian(
            clampedPosition,
            scratchUpdateHeightCartesian,
        );

        const clampedModelMatrix = model._clampedModelMatrix;

        // Modify clamped model matrix to use new height
        Cesium.Matrix4.clone(model.modelMatrix, clampedModelMatrix);
        clampedModelMatrix[12] = scratchUpdateHeightCartesian.x;
        clampedModelMatrix[13] = scratchUpdateHeightCartesian.y;
        clampedModelMatrix[14] = scratchUpdateHeightCartesian.z;

        model._heightDirty = true;
    };
}
function updateBoundingSphereAndScale(model: any, frameState: any) {
    if (!model._updateModelMatrix && model._minimumPixelSize === 0.0) {
        return;
    }

    const modelMatrix = Cesium.defined(model._clampedModelMatrix)
        ? model._clampedModelMatrix
        : model.modelMatrix;

    updateBoundingSphere(model, modelMatrix);
    updateComputedScale(model, modelMatrix, frameState);
}

function updateBoundingSphere(model: any, modelMatrix: any) {
    model._clampedScale = Cesium.defined(model._maximumScale)
        ? Math.min(model._scale, model._maximumScale)
        : model._scale;

    model._boundingSphere.center = Cesium.Cartesian3.multiplyByScalar(
        model._sceneGraph.boundingSphere.center,
        model._clampedScale,
        model._boundingSphere.center,
    );
    model._boundingSphere.radius = model._initialRadius * model._clampedScale;

    model._boundingSphere = Cesium.BoundingSphere.transform(
        model._boundingSphere,
        modelMatrix,
        model._boundingSphere,
    );
}

function updateComputedScale(model: any, modelMatrix: any, frameState: any) {
    let scale = model.scale;

    if (model.minimumPixelSize !== 0.0 && !model._projectTo2D) {
        // Compute size of bounding sphere in pixels
        const context = frameState.context;
        const maxPixelSize = Math.max(
            context.drawingBufferWidth,
            context.drawingBufferHeight,
        );

        Cesium.Matrix4.getTranslation(modelMatrix, scratchPosition);

        if (model._sceneMode !== Cesium.SceneMode.SCENE3D) {
            //@ts-ignore
            Cesium.SceneTransforms.computeActualEllipsoidPosition(
                frameState,
                scratchPosition,
                scratchPosition,
            );
        }

        const radius = model._boundingSphere.radius;
        const metersPerPixel = scaleInPixels(scratchPosition, radius, frameState);

        // metersPerPixel is always > 0.0
        const pixelsPerMeter = 1.0 / metersPerPixel;
        const diameterInPixels = Math.min(
            pixelsPerMeter * (2.0 * radius),
            maxPixelSize,
        );

        // Maintain model's minimum pixel size
        if (diameterInPixels < model.minimumPixelSize) {
            scale =
                (model.minimumPixelSize * metersPerPixel) /
                (2.0 * model._initialRadius);
        }
    }

    model._computedScale = Cesium.defined(model.maximumScale)
        ? Math.min(model.maximumScale, scale)
        : scale;
}
const scratchBoundingSphere = new Cesium.BoundingSphere();
function scaleInPixels(positionWC: any, radius: any, frameState: any) {
    scratchBoundingSphere.center = positionWC;
    scratchBoundingSphere.radius = radius;
    return frameState.camera.getPixelSize(
        scratchBoundingSphere,
        frameState.context.drawingBufferWidth,
        frameState.context.drawingBufferHeight,
    );
}
function updatePickIds(model: any) {
    if (!model._idDirty) {
        return;
    }
    model._idDirty = false;

    const id = model._id;
    const pickIds = model._pickIds;
    const length = pickIds.length;
    for (let i = 0; i < length; ++i) {
        pickIds[i].object.id = id;
    }
}

// Matrix3 is a row-major constructor.
// The same constructor in GLSL will produce the transpose of this.
const yUpToZUp = new Cesium.Matrix3(1, 0, 0, 0, 0, 1, 0, -1, 0);
const scratchIBLReferenceFrameMatrix4 = new Cesium.Matrix4();
const scratchIBLReferenceFrameMatrix3 = new Cesium.Matrix3();
const scratchClippingPlanesMatrix = new Cesium.Matrix4();
function updateReferenceMatrices(model: any, frameState: any) {
    const modelMatrix = Cesium.defined(model._clampedModelMatrix)
        ? model._clampedModelMatrix
        : model.modelMatrix;
    const referenceMatrix = Cesium.defaultValue(model.referenceMatrix, modelMatrix);
    const context = frameState.context;

    let iblReferenceFrameMatrix3 = scratchIBLReferenceFrameMatrix3;
    let iblReferenceFrameMatrix4 = scratchIBLReferenceFrameMatrix4;

    iblReferenceFrameMatrix4 = Cesium.Matrix4.multiply(
        context.uniformState.view3D,
        referenceMatrix,
        iblReferenceFrameMatrix4,
    );
    iblReferenceFrameMatrix3 = Cesium.Matrix4.getRotation(
        iblReferenceFrameMatrix4,
        iblReferenceFrameMatrix3,
    );
    iblReferenceFrameMatrix3 = Cesium.Matrix3.transpose(
        iblReferenceFrameMatrix3,
        iblReferenceFrameMatrix3,
    );
    model._iblReferenceFrameMatrix = Cesium.Matrix3.multiply(
        yUpToZUp,
        iblReferenceFrameMatrix3,
        model._iblReferenceFrameMatrix,
    );

    if (model.isClippingEnabled()) {
        let clippingPlanesMatrix = scratchClippingPlanesMatrix;
        clippingPlanesMatrix = Cesium.Matrix4.multiply(
            context.uniformState.view3D,
            referenceMatrix,
            clippingPlanesMatrix,
        );
        clippingPlanesMatrix = Cesium.Matrix4.multiply(
            clippingPlanesMatrix,
            model._clippingPlanes.modelMatrix,
            clippingPlanesMatrix,
        );
        model._clippingPlanesMatrix = Cesium.Matrix4.inverseTranspose(
            clippingPlanesMatrix,
            model._clippingPlanesMatrix,
        );
    }
}

function updateSceneGraph(model: any, frameState: any) {
    const sceneGraph = model._sceneGraph;
    if (model._updateModelMatrix || model._minimumPixelSize !== 0.0) {
        const modelMatrix = Cesium.defined(model._clampedModelMatrix)
            ? model._clampedModelMatrix
            : model.modelMatrix;
        sceneGraph.updateModelMatrix(modelMatrix, frameState);
        model._updateModelMatrix = false;
    }

    if (model._backFaceCullingDirty) {
        sceneGraph.updateBackFaceCulling(model._backFaceCulling);
        model._backFaceCullingDirty = false;
    }

    if (model._shadowsDirty) {
        sceneGraph.updateShadows(model._shadows);
        model._shadowsDirty = false;
    }

    if (model._debugShowBoundingVolumeDirty) {
        sceneGraph.updateShowBoundingVolume(model._debugShowBoundingVolume);
        model._debugShowBoundingVolumeDirty = false;
    }

    let updateForAnimations = false;
    // Animations are disabled for classification models.
    if (!Cesium.defined(model.classificationType)) {
        updateForAnimations =
            model._userAnimationDirty || model._activeAnimations.update(frameState);
    }
    sceneGraph.update(frameState, updateForAnimations);
    model._userAnimationDirty = false;
}

function updateShowCreditsOnScreen(model: any) {
    if (!model._showCreditsOnScreenDirty) {
        return;
    }
    model._showCreditsOnScreenDirty = false;
    model._credits.length = 0;

    const showOnScreen = model._showCreditsOnScreen;
    if (Cesium.defined(model._credit)) {
        const credit = Cesium.Credit.clone(model._credit);
        credit.showOnScreen = credit.showOnScreen || showOnScreen;
        model._credits.push(credit);
    }

    const resourceCredits = model._resourceCredits;
    const resourceCreditsLength = resourceCredits.length;
    for (let i = 0; i < resourceCreditsLength; i++) {
        const credit = Cesium.Credit.clone(resourceCredits[i]);
        credit.showOnScreen = credit.showOnScreen || showOnScreen;
        model._credits.push(credit);
    }

    const gltfCredits = model._gltfCredits;
    const gltfCreditsLength = gltfCredits.length;
    for (let i = 0; i < gltfCreditsLength; i++) {
        const credit = Cesium.Credit.clone(gltfCredits[i]);
        credit.showOnScreen = credit.showOnScreen || showOnScreen;
        model._credits.push(credit);
    }
}

function submitDrawCommands(model: any, frameState: any) {
    // Check that show is true after draw commands are built;
    // we want the user to be able to instantly see the model
    // when show is set to true.

    const displayConditionPassed = passesDistanceDisplayCondition(
        model,
        frameState,
    );

    const invisible = model.isInvisible();
    const silhouette = model.hasSilhouette(frameState);

    // If the model is invisible but has a silhouette, it still
    // needs to draw in order to write to the stencil buffer and
    // render the silhouette.
    const showModel =
        model._show &&
        model._computedScale !== 0 &&
        displayConditionPassed &&
        (!invisible || silhouette);

    const passes = frameState.passes;
    const submitCommandsForPass =
        passes.render || (passes.pick && model.allowPicking);

    if (showModel && !model._ignoreCommands && submitCommandsForPass) {
        addCreditsToCreditDisplay(model, frameState);
        model._sceneGraph.pushDrawCommands(frameState);
    }
}
function addCreditsToCreditDisplay(model: any, frameState: any) {
    const creditDisplay = frameState.creditDisplay;
    const credits = model._credits;
    const creditsLength = credits.length;
    for (let c = 0; c < creditsLength; c++) {
        creditDisplay.addCreditToNextFrame(credits[c]);
    }
}
const scratchDisplayConditionCartesian = new Cesium.Cartesian3();
function passesDistanceDisplayCondition(model: any, frameState: any) {
    const condition = model.distanceDisplayCondition;
    if (!Cesium.defined(condition)) {
        return true;
    }

    const nearSquared = condition.near * condition.near;
    const farSquared = condition.far * condition.far;
    let distanceSquared;

    if (frameState.mode === Cesium.SceneMode.SCENE2D) {
        const frustum2DWidth =
            frameState.camera.frustum.right - frameState.camera.frustum.left;
        const distance = frustum2DWidth * 0.5;
        distanceSquared = distance * distance;
    } else {
        // Distance to center of primitive's reference frame
        const position = Cesium.Matrix4.getTranslation(
            model.modelMatrix,
            scratchDisplayConditionCartesian,
        );

        // This will project the position if the scene is in Columbus View,
        // but leave the position as-is in 3D mode.
        //@ts-ignore
        Cesium.SceneTransforms.computeActualEllipsoidPosition(
            frameState,
            position,
            position,
        );
        distanceSquared = Cesium.Cartesian3.distanceSquared(
            position,
            frameState.camera.positionWC,
        );
    }

    return distanceSquared >= nearSquared && distanceSquared <= farSquared;
}
function isHeightReferenceRelative(heightReference: any) {
    return (
        heightReference === Cesium.HeightReference.RELATIVE_TO_GROUND ||
        heightReference === Cesium.HeightReference.RELATIVE_TO_3D_TILE ||
        heightReference === Cesium.HeightReference.RELATIVE_TO_TERRAIN
    );
}
//#endregion
